/**
	License:		Artistic license 2.0 (see license.txt, also available online at http://www.perlfoundation.org/artistic_license_2_0)
	Authors:		OrbitalLab (http://www.orbitallab.ru/moonglide/), 2007-2009

	File:			Config.d
	Description:	simple configuration files support
	Date:			8.04.2008 by Digited
*/
module moonglide.io.Config;

import
	moonglide.math.MoonMath,

	tango.io.Stdout,
	tango.io.stream.Lines,

	moonglide.util.MoonUtil,

	moonglide.io.MoonFile,
	moonglide.io.LogSystem;

debug import moonglide.util.Regex;
else
 import tango.text.Regex;

//=============================================================
/// config data field type enum
//=============================================================
enum CONFIGFIELDTYPE
{
	Boolean,
	Int,
	Float,
	String
}

//=============================================================
/// config field entry struct
//=============================================================
struct SConfigFieldEntry
{
	char[] Field;
	CONFIGFIELDTYPE Type;

	static SConfigFieldEntry opCall( char[] f, CONFIGFIELDTYPE t )
	{
		SConfigFieldEntry cfe;
		cfe.Field = f;
		cfe.Type = t;
		return cfe;
	}
}

//=============================================================
/// simple class to work with config files (.cfg)
//=============================================================
class CConfig
{
	protected:
		static Regex
			testreg,
			cutreg,
			headerreg,
			headercutreg,
			equalcheckreg,
			equalreg,
			stringreg,
			bracketreg;

		static this()
		{
			try
			{
				testreg			= Regex( "\".*\"");
				cutreg			= Regex( "(\\s+)|(;.*)" );
				headerreg		= Regex( r"^\[.+\].*" );
				headercutreg	= Regex( r"[\[]|[\]]" );
				equalcheckreg	= Regex( "^.+=.+;.*$" );
				stringreg		= Regex( "\".*\"" );
				bracketreg		= Regex( "\"" );
			}
			catch( Exception e )
				errorLog( "static regex construction catched an error: " ~ e.msg );
		}


		void TrimLine( inout char[] str )
		{
			// remove whitespaces, tabs, ;-ending and everything after it, but not for ""-value
			if( testreg.test( str ) )
				str = cutreg.replaceAll( testreg.pre, "" ) ~ testreg.match(0) ~
					cutreg.replaceAll( testreg.post, "" );
			else
				str = cutreg.replaceAll( str, "" );
		}

		//=============================================================
		/// parse lines to config internal struct
		//=============================================================
		void Parse( char[] line )
		{
			static char[] groupName = "";

			if( line.length == 0 )
			{
				groupName = "";
				return;
			}

			if( line[0] == ' ' || line[0] == '\n' || line[0] == '\t' )
			{
				groupName = "";
				return;
			}

			if( headerreg.test( line ) ) // [group header]
			{
				// new group
				groupName = headercutreg.replaceAll( line, "" ); // get header name by removing [ and ]
				_groups[ groupName ] = new SConfigFieldEntry[0];
				return;
			}

			// check if string contains name, =, value and ; with zero-or-any chars after it
			if( !equalcheckreg.test( line ) )
				return;

			// get name and value
			TrimLine( line );		// now we have "name=value"

			static char[] field, value;
			static char[][] splitresult;

			splitresult = tango.text.Util.split( line, "=" );			// according of check regexp, line must be "name=value"
			field = splitresult[0];
			value = splitresult[1];

			if(( value == "true" ) || ( value == "false" )) // boolean
			{
				bool boolval = (value == "true");
				mBoolFields[field] = boolval;
				_groups[ groupName ] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Boolean );
			}
			else
				if( stringreg.test( value ) ) // string, can be empty
				{
					value = bracketreg.replaceAll( value, "" );
					mStringFields[ field ] = value;
					_groups[ groupName ] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.String );
				}
				else
					if( value[ $-1 ] == 'f' ) // Floating point number
					{
						float floatvar = strToFloat( value[0..$-1] );
						mFloatFields[ field ] = floatvar;
						_groups[ groupName ] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Float );
					}
					else // Integer (?)
					{
						int intvar = strToInt( value );
						mIntFields[ field ] = intvar;
						_groups[ groupName ] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Int );
					}
		}

	public:
		alias int[ char[] ]		IntContainer;
		alias bool[ char[] ]	BoolContainer;
		alias float[ char[] ]	FloatContainer;
		alias char[][ char[] ]	StringContainer;

		SConfigFieldEntry[][ char[] ]
								_groups;

		IntContainer			mIntFields;
		BoolContainer			mBoolFields;
		FloatContainer			mFloatFields;
		StringContainer			mStringFields;

		//================================================================
		/// constructor & destructor stuff
		//================================================================
		this()
		{
			// we must have at least an unnamed (default) group
			_groups[ "" ] = new SConfigFieldEntry[0];
		}

		this( ref InputStream stream )
		{
			this();
			load( stream );
		}

		//=============================================================
		/// loads config from file with filename "configFileName", using CMoonFile
		//=============================================================
		this( char[] configFileName )
		{
			this();

			if( configFileName.length == 0 )
				return;

			try
			{
				auto configFile = CMoonFile().file( configFileName );

				if( configFile.exists )
				{
					auto stream = configFile.input;
					load( stream );
					traceLog.trace( "file " ~ configFileName ~ " found and loaded" );
				}
				else
					errorLog( "config file not found: " ~ configFileName );
			}
			catch( Exception e )
			{
				errorLog( e.toString );
			}
		}

		//=============================================================
		/// opCall for ease of use: auto config = CConfig( "ConfigFileName.txt" );
		//=============================================================
		static CConfig opCall( char[] configFileName )
		{
			return new CConfig( configFileName );
		}

		~this() {}

		//=============================================================
		/// loads config from input stream and parses it
		//=============================================================
		void load( ref InputStream stream )
		{
			foreach( line; new Lines!(char)( stream ) )
				Parse( line );

			stream.close;
		}


		//=============================================================
		/// saves current config data to a file "configFileName"
		/// NOTE: if file configFileName exists, it will be overwritten!
		//=============================================================
		void save( char[] configFileName )
		{
			//
		}

		//=============================================================
		/// saves config to single text string that is written then to given stream with a single write call
		/// NOTE: given stream is flushed and closed at end!
		//=============================================================
		void save( OutputStream stream )
		{
			char[] streamline;

			foreach( name, cfeArray; _groups )
			{
				if( name.length > 0 )		// [name]
					streamline ~= Format( r"[{}]\n", name );

				foreach( CnfFldEntry; cfeArray )
				{
					switch( CnfFldEntry.Type )
					{
						case CONFIGFIELDTYPE.Int:
							streamline ~= Format( "{}={};\n", CnfFldEntry.Field, mIntFields[ CnfFldEntry.Field ] );
							mIntFields.remove( CnfFldEntry.Field );
							break;

						case CONFIGFIELDTYPE.Float:
							streamline ~= Format( "{}={}f;\n", CnfFldEntry.Field, mFloatFields[ CnfFldEntry.Field ] );
							mFloatFields.remove( CnfFldEntry.Field );
							break;

						case CONFIGFIELDTYPE.String:
							streamline ~= Format( "{}=\"{}\";\n", CnfFldEntry.Field, mStringFields[ CnfFldEntry.Field ] );
							mStringFields.remove( CnfFldEntry.Field );
							break;

						case CONFIGFIELDTYPE.Boolean:
							static char[] bch;
							bch = mBoolFields[ CnfFldEntry.Field ] ? "true" : "false";
							streamline ~= Format( "{}={};\n", CnfFldEntry.Field, bch );
							mBoolFields.remove( CnfFldEntry.Field );
							break;

						default: break;
					}
				}

				streamline ~= "\n";
			}

			stream.write( streamline );
			stream.flush.close;
		}

		//=============================================================
		/// get stuff
		//=============================================================
		bool getBool( char[] field ) { return mBoolFields[ field ]; }
		bool getBool( char[] field, bool defaultValue )	///ditto
		{
			if( (field in mBoolFields ) == null )
			{
				mBoolFields[ field ] = defaultValue;
				_groups[""] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Boolean );
			}

			return mBoolFields[ field ];
		}

		int getInteger( char[] field ) { return mIntFields[ field ]; } ///ditto
		int getInteger( char[] field, int defaultValue )		///ditto
		{
			if( ( field in mIntFields ) == null )
			{
				mIntFields[ field ] = defaultValue;
				_groups[""] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Int );
			}

			return mIntFields[ field ];
		}

		float getFloat( char[] field ) { return mFloatFields[ field ]; }	///ditto
		float getFloat( char[] field, float defaultValue )		///ditto
		{
			if( ( field in mFloatFields ) == null )
			{
				mFloatFields[ field ] = defaultValue;
				_groups[""] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.Float );
			}

			return mFloatFields[ field ];
		}

		char[] getString( char[] field )
		{
//			debug
//			{
//				char[] result = mStringFields[ field ];
//				debugLog.trace( result );
//				return result;
//			}
//			else
			moonAssert( ( field in mStringFields ) !is null, "cannot find key <" ~ field ~ "> in config.getString() method", __FILE__, __LINE__ );

			return mStringFields[ field ];
		}
		char[] getString( char[] field, char[] defaultValue )		///ditto
		{
			if( ( field in mStringFields ) is null )
			{
				mStringFields[ field ] = defaultValue;
				_groups[""] ~= SConfigFieldEntry( field, CONFIGFIELDTYPE.String );
			}

			return mStringFields[ field ];
		}
}
